﻿/*******************************************************************************
 * You may amend and distribute as you like, but don't remove this header!
 *
 * EPPlus provides server-side generation of Excel 2007/2010 spreadsheets.
 * See https://github.com/JanKallman/EPPlus for details.
 *
 * Copyright (C) 2011  Jan Källman
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.

 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
 * See the GNU Lesser General Public License for more details.
 *
 * The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php
 * If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html
 *
 * All code and executables are provided "as is" with no warranty either express or implied. 
 * The author accepts no liability for any damage or loss of business that this product may cause.
 *
 * Code change notes:
 * 
 * Author							Change						Date
 * ******************************************************************************
 * Eyal Seagull        Added       		  2012-04-03
 *******************************************************************************/
using OfficeOpenXml.ConditionalFormatting.Contracts;
using OfficeOpenXml.Style.Dxf;
using OfficeOpenXml.Utils;
using System;
using System.Xml;

namespace OfficeOpenXml.ConditionalFormatting
{
    /// <summary>
    /// 
    /// </summary>
    public abstract class ExcelConditionalFormattingRule
    : XmlHelper,
    IExcelConditionalFormattingRule
    {
        /****************************************************************************************/

        #region Private Properties
        private eExcelConditionalFormattingRuleType? _type;
        private ExcelWorksheet _worksheet;

        /// <summary>
        /// Sinalize that we are in a Cnaging Priorities opeartion so that we won't enter
        /// a recursive loop.
        /// </summary>
        private static bool _changingPriority = false;
        #endregion Private Properties

        /****************************************************************************************/

        #region Constructors
        /// <summary>
        /// Initialize the <see cref="ExcelConditionalFormattingRule"/>
        /// </summary>
        /// <param name="type"></param>
        /// <param name="address"></param>
        /// <param name="priority">Used also as the cfRule unique key</param>
        /// <param name="worksheet"></param>
        /// <param name="itemElementNode"></param>
        /// <param name="namespaceManager"></param>
        internal ExcelConditionalFormattingRule(
          eExcelConditionalFormattingRuleType type,
          ExcelAddress address,
          int priority,
          ExcelWorksheet worksheet,
          XmlNode itemElementNode,
          XmlNamespaceManager namespaceManager)
          : base(
            namespaceManager,
            itemElementNode)
        {
            Require.Argument(address).IsNotNull("address");

            // While MSDN states that 1 is the "highest priority," it also defines this
            // field as W3C XML Schema int, which would allow values less than 1. Excel
            // itself will, on occasion, use a value of 0, so this check will allow a 0.
            Require.Argument(priority).IsInRange(0, int.MaxValue, "priority");

            Require.Argument(worksheet).IsNotNull("worksheet");

            _type = type;
            _worksheet = worksheet;
            SchemaNodeOrder = _worksheet.SchemaNodeOrder;

            if (itemElementNode == null)
            {
                // Create/Get the <cfRule> inside <conditionalFormatting>
                itemElementNode = CreateComplexNode(
                  _worksheet.WorksheetXml.DocumentElement,
                  string.Format(
                    "{0}[{1}='{2}']/{1}='{2}'/{3}[{4}='{5}']/{4}='{5}'",
                    //{0}
                    ExcelConditionalFormattingConstants.Paths.ConditionalFormatting,
                    // {1}
                    ExcelConditionalFormattingConstants.Paths.SqrefAttribute,
                    // {2}
                    address.AddressSpaceSeparated,          //CF node don't what to have comma between multi addresses, use space instead.
                                                            // {3}
                    ExcelConditionalFormattingConstants.Paths.CfRule,
                    //{4}
                    ExcelConditionalFormattingConstants.Paths.PriorityAttribute,
                    //{5}
                    priority));
            }

            // Point to <cfRule>
            TopNode = itemElementNode;

            Address = address;
            Priority = priority;
            Type = type;
            if (DxfId >= 0)
            {
                worksheet.Workbook.Styles.Dxfs[DxfId].AllowChange = true;  //This Id is referenced by CF, so we can use it when we save.
                _style = worksheet.Workbook.Styles.Dxfs[DxfId].Clone();    //Clone, so it can be altered without effecting other dxf styles
            }
        }

        /// <summary>
        /// Initialize the <see cref="ExcelConditionalFormattingRule"/>
        /// </summary>
        /// <param name="type"></param>
        /// <param name="address"></param>
        /// <param name="priority"></param>
        /// <param name="worksheet"></param>
        /// <param name="namespaceManager"></param>
        internal ExcelConditionalFormattingRule(
          eExcelConditionalFormattingRuleType type,
          ExcelAddress address,
          int priority,
          ExcelWorksheet worksheet,
          XmlNamespaceManager namespaceManager)
          : this(
            type,
            address,
            priority,
            worksheet,
            null,
            namespaceManager)
        {
        }
        #endregion Constructors

        /****************************************************************************************/

        #region Methods
        #endregion Methods

        /****************************************************************************************/

        #region Exposed Properties
        /// <summary>
        /// Get the &lt;cfRule&gt; node
        /// </summary>
        public XmlNode Node
        {
            get { return TopNode; }
        }

        /// <summary>
        /// Address of the conditional formatting rule
        /// </summary>
        /// <remarks>
        /// The address is stores in a parent node called &lt;conditionalFormatting&gt; in the
        /// @sqref attribute. Excel groups rules that have the same address inside one node.
        /// </remarks>
        public ExcelAddress Address
        {
            get
            {
                return new ExcelAddress(
                  Node.ParentNode.Attributes[ExcelConditionalFormattingConstants.Attributes.Sqref].Value);
            }
            set
            {
                // Check if the address is to be changed
                if (Address.Address != value.Address)
                {
                    // Save the old parente node
                    XmlNode oldNode = Node;
                    XmlNode oldParentNode = Node.ParentNode;

                    // Create/Get the new <conditionalFormatting> parent node
                    XmlNode newParentNode = CreateComplexNode(
                      _worksheet.WorksheetXml.DocumentElement,
                      string.Format(
                        "{0}[{1}='{2}']/{1}='{2}'",
                        //{0}
                        ExcelConditionalFormattingConstants.Paths.ConditionalFormatting,
                        // {1}
                        ExcelConditionalFormattingConstants.Paths.SqrefAttribute,
                        // {2}
                        value.AddressSpaceSeparated));

                    // Move the <cfRule> node to the new <conditionalFormatting> parent node
                    TopNode = newParentNode.AppendChild(Node);

                    // Check if the old <conditionalFormatting> parent node has <cfRule> node inside it
                    if (!oldParentNode.HasChildNodes)
                    {
                        // Remove the old parent node
                        oldParentNode.ParentNode.RemoveChild(oldParentNode);
                    }
                }
            }
        }

        /// <summary>
        /// Type of conditional formatting rule. ST_CfType §18.18.12.
        /// </summary>
        public eExcelConditionalFormattingRuleType Type
        {
            get
            {
                // Transform the @type attribute to EPPlus Rule Type (slighty diferente)
                if (_type == null)
                {
                    _type = ExcelConditionalFormattingRuleType.GetTypeByAttrbiute(
                    GetXmlNodeString(ExcelConditionalFormattingConstants.Paths.TypeAttribute),
                    TopNode,
                    _worksheet.NameSpaceManager);
                }
                return (eExcelConditionalFormattingRuleType)_type;
            }
            internal set
            {
                _type = value;
                // Transform the EPPlus Rule Type to @type attribute (slighty diferente)
                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.TypeAttribute,
                  ExcelConditionalFormattingRuleType.GetAttributeByType(value),
                  true);
            }
        }

        /// <summary>
        /// The priority of this conditional formatting rule. This value is used to determine
        /// which format should be evaluated and rendered. Lower numeric values are higher
        /// priority than higher numeric values, where 1 is the highest priority.
        /// </summary>
        public int Priority
        {
            get
            {
                return GetXmlNodeInt(
                  ExcelConditionalFormattingConstants.Paths.PriorityAttribute);
            }
            set
            {
                // Save the current CF rule priority
                int priority = Priority;

                // Check if the @priority is to be changed
                if (priority != value)
                {
                    // Check if we are not already inside a "Change Priority" operation
                    if (!_changingPriority)
                    {
                        if (value < 1)
                        {
                            throw new IndexOutOfRangeException(
                              ExcelConditionalFormattingConstants.Errors.InvalidPriority);
                        }

                        // Sinalize that we are already changing cfRules priorities
                        _changingPriority = true;

                        // Check if we lowered the priority
                        if (priority > value)
                        {
                            for (int i = priority - 1; i >= value; i--)
                            {
                                var cfRule = _worksheet.ConditionalFormatting.RulesByPriority(i);

                                if (cfRule != null)
                                {
                                    cfRule.Priority++;
                                }
                            }
                        }
                        else
                        {
                            for (int i = priority + 1; i <= value; i++)
                            {
                                var cfRule = _worksheet.ConditionalFormatting.RulesByPriority(i);

                                if (cfRule != null)
                                {
                                    cfRule.Priority--;
                                }
                            }
                        }

                        // Sinalize that we are no longer changing cfRules priorities
                        _changingPriority = false;
                    }

                    // Change the priority in the XML
                    SetXmlNodeString(
                      ExcelConditionalFormattingConstants.Paths.PriorityAttribute,
                      value.ToString(),
                      true);
                }
            }
        }

        /// <summary>
        /// If this flag is true, no rules with lower priority shall be applied over this rule,
        /// when this rule evaluates to true.
        /// </summary>
        public bool StopIfTrue
        {
            get
            {
                return GetXmlNodeBool(
                  ExcelConditionalFormattingConstants.Paths.StopIfTrueAttribute);
            }
            set
            {
                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.StopIfTrueAttribute,
                  (value == true) ? "1" : string.Empty,
                  true);
            }
        }

        /// <summary>
        /// DxfId Style Attribute
        /// </summary>
        internal int DxfId
        {
            get
            {
                return GetXmlNodeInt(
                  ExcelConditionalFormattingConstants.Paths.DxfIdAttribute);
            }
            set
            {
                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.DxfIdAttribute,
                  (value == int.MinValue) ? string.Empty : value.ToString(),
                  true);
            }
        }
        internal ExcelDxfStyleConditionalFormatting _style = null;
        public ExcelDxfStyleConditionalFormatting Style
        {
            get
            {
                if (_style == null)
                {
                    _style = new ExcelDxfStyleConditionalFormatting(NameSpaceManager, null, _worksheet.Workbook.Styles);
                }
                return _style;
            }
        }
        /// <summary>
        /// StdDev (zero is not allowed and will be converted to 1)
        /// </summary>
        public UInt16 StdDev
        {
            get
            {
                return Convert.ToUInt16(GetXmlNodeInt(
                  ExcelConditionalFormattingConstants.Paths.StdDevAttribute));
            }
            set
            {
                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.StdDevAttribute,
                  (value == 0) ? "1" : value.ToString(),
                  true);
            }
        }

        /// <summary>
        /// Rank (zero is not allowed and will be converted to 1)
        /// </summary>
        public UInt16 Rank
        {
            get
            {
                return Convert.ToUInt16(GetXmlNodeInt(
                  ExcelConditionalFormattingConstants.Paths.RankAttribute));
            }
            set
            {
                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.RankAttribute,
                  (value == 0) ? "1" : value.ToString(),
                  true);
            }
        }
        #endregion Exposed Properties

        /****************************************************************************************/

        #region Internal Properties
        /// <summary>
        /// AboveAverage
        /// </summary>
        internal protected bool? AboveAverage
        {
            get
            {
                bool? aboveAverage = GetXmlNodeBoolNullable(
                  ExcelConditionalFormattingConstants.Paths.AboveAverageAttribute);

                // Above Avarege if TRUE or if attribute does not exists
                return (aboveAverage == true) || (aboveAverage == null);
            }
            set
            {
                string aboveAverageValue = string.Empty;

                // Only the types that needs the @AboveAverage
                if ((_type == eExcelConditionalFormattingRuleType.BelowAverage)
                  || (_type == eExcelConditionalFormattingRuleType.BelowOrEqualAverage)
                  || (_type == eExcelConditionalFormattingRuleType.BelowStdDev))
                {
                    aboveAverageValue = "0";
                }

                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.AboveAverageAttribute,
                  aboveAverageValue,
                  true);
            }
        }

        /// <summary>
        /// EqualAverage
        /// </summary>
        internal protected bool? EqualAverage
        {
            get
            {
                bool? equalAverage = GetXmlNodeBoolNullable(
                  ExcelConditionalFormattingConstants.Paths.EqualAverageAttribute);

                // Equal Avarege only if TRUE
                return (equalAverage == true);
            }
            set
            {
                string equalAverageValue = string.Empty;

                // Only the types that needs the @EqualAverage
                if ((_type == eExcelConditionalFormattingRuleType.AboveOrEqualAverage)
                  || (_type == eExcelConditionalFormattingRuleType.BelowOrEqualAverage))
                {
                    equalAverageValue = "1";
                }

                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.EqualAverageAttribute,
                  equalAverageValue,
                  true);
            }
        }

        /// <summary>
        /// Bottom attribute
        /// </summary>
        internal protected bool? Bottom
        {
            get
            {
                bool? bottom = GetXmlNodeBoolNullable(
                  ExcelConditionalFormattingConstants.Paths.BottomAttribute);

                // Bottom if TRUE
                return (bottom == true);
            }
            set
            {
                string bottomValue = string.Empty;

                // Only the types that needs the @Bottom
                if ((_type == eExcelConditionalFormattingRuleType.Bottom)
                  || (_type == eExcelConditionalFormattingRuleType.BottomPercent))
                {
                    bottomValue = "1";
                }

                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.BottomAttribute,
                  bottomValue,
                  true);
            }
        }

        /// <summary>
        /// Percent attribute
        /// </summary>
        internal protected bool? Percent
        {
            get
            {
                bool? percent = GetXmlNodeBoolNullable(
                  ExcelConditionalFormattingConstants.Paths.PercentAttribute);

                // Bottom if TRUE
                return (percent == true);
            }
            set
            {
                string percentValue = string.Empty;

                // Only the types that needs the @Bottom
                if ((_type == eExcelConditionalFormattingRuleType.BottomPercent)
                  || (_type == eExcelConditionalFormattingRuleType.TopPercent))
                {
                    percentValue = "1";
                }

                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.PercentAttribute,
                  percentValue,
                  true);
            }
        }

        /// <summary>
        /// TimePeriod
        /// </summary>
        internal protected eExcelConditionalFormattingTimePeriodType TimePeriod
        {
            get
            {
                return ExcelConditionalFormattingTimePeriodType.GetTypeByAttribute(
                  GetXmlNodeString(ExcelConditionalFormattingConstants.Paths.TimePeriodAttribute));
            }
            set
            {
                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.TimePeriodAttribute,
                  ExcelConditionalFormattingTimePeriodType.GetAttributeByType(value),
                  true);
            }
        }

        /// <summary>
        /// Operator
        /// </summary>
        internal protected eExcelConditionalFormattingOperatorType Operator
        {
            get
            {
                return ExcelConditionalFormattingOperatorType.GetTypeByAttribute(
                  GetXmlNodeString(ExcelConditionalFormattingConstants.Paths.OperatorAttribute));
            }
            set
            {
                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.OperatorAttribute,
                  ExcelConditionalFormattingOperatorType.GetAttributeByType(value),
                  true);
            }
        }

        /// <summary>
        /// Formula
        /// </summary>
        public string Formula
        {
            get
            {
                return GetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.Formula);
            }
            set
            {
                SetXmlNodeString(
                  ExcelConditionalFormattingConstants.Paths.Formula,
                  value);
            }
        }

        /// <summary>
        /// Formula2
        /// </summary>
        public string Formula2
        {
            get
            {
                return GetXmlNodeString(
                  string.Format(
                    "{0}[position()=2]",
                    // {0}
                    ExcelConditionalFormattingConstants.Paths.Formula));
            }
            set
            {
                // Create/Get the first <formula> node (ensure that it exists)
                var firstNode = CreateComplexNode(
                  TopNode,
                  string.Format(
                    "{0}[position()=1]",
                    // {0}
                    ExcelConditionalFormattingConstants.Paths.Formula));

                // Create/Get the seconde <formula> node (ensure that it exists)
                var secondNode = CreateComplexNode(
                  TopNode,
                  string.Format(
                    "{0}[position()=2]",
                    // {0}
                    ExcelConditionalFormattingConstants.Paths.Formula));

                // Save the formula in the second <formula> node
                secondNode.InnerText = value;
            }
        }
        #endregion Internal Properties
        /****************************************************************************************/
    }
}